---
description: 怎么更加面向对象
---

面向对象的三个特征是封装、继承和多态。其中多态我们暂时还不涉及。而上节我们完成的添加组织功能，在封装和继承方面还不到位。

今天我们会继续开发修改组织功能，结合这个功能的开发，看看怎样运用封装和继承。为“修改组织”功能开卡。

## 为修改组织功能开卡

为了完成修改组织的功能，我们再一次把产品经理老王请过来进行“开卡”，看需求的细节是否理解到位。

首先要确认的一个问题是，到底要修改组织的哪些信息。目前的 Org 对象一共有 11 个属性，我们列成一个表，和老王逐一确认。

<img src="https://wkq-img.oss-cn-chengdu.aliyuncs.com/20241203001929.png"/>

这个表的含义是，在完成某个功能时，客户端要提供哪些数据。其中“创建”一栏是创建组织时需要哪些参数。“修改”一栏中的问号，是我们觉得在修改时需要客户提供，但需要与业务确认的。

1. 如果修改“上级组织”的话，实际上是在调整组织结构。一般不作为普通的修改功能，建议增加专门的“调整组织结构”功能。

2. “组织类别”不能修改，也就是说，一个开发组不能直接变成开发中心。如果真有这种需求，需要另外创建新的开发中心，把人迁移过去，然后撤销当前的开发组。

3. “状态”不能直接改，应该通过“撤销组织”功能间接修改。

4. 最终只有“负责人”和“名称”可以直接修改，建议这个功能叫做“修改组织基本信息”。

根据老王的建议，我们把表格改成了下面的样子。由于调整组织结构功能比较复杂，所以以后再考虑，这里只增加了“撤销组织”功能。

<img src="https://wkq-img.oss-cn-chengdu.aliyuncs.com/20241203002227.png"/>

接着，老王又为撤销组织增加了 2 个业务规则。

<img src="https://wkq-img.oss-cn-chengdu.aliyuncs.com/20241203002354.png"/>

## 初步完成程序功能

修改后的 OrgServcie 是这样的。

```java
package application.orgmng;
// imports ...

@Service
public class OrgService {
    private final OrgBuilderFactory orgBuilderFactory;
    private final OrgRepository orgRepository;
    private final OrgHandler orgHandler;  //新增了一个领域服务依赖

    @Autowired
    public OrgService(OrgBuilderFactory orgBuilderFactory
            , OrgHandler orgHandler
            , OrgRepository orgRepository) {
        // 为依赖注入赋值 ...
    }

    @Transactional
    public OrgDto addOrg(OrgDto request，Long userId) {
        // 添加组织的功能已完成，这里省略 ...
    }

    //修改组织基本信息
    @Transactional
    public OrgDto updateOrgBasic(Long id, OrgDto request, Long userId) {
        Org org = orgRepository.findById(request.getTenant(), id)
                .orElseThrow(() -> {
                    throw new BusinessException("要修改的组织(id ="
                                + id + "  )不存在！");
                });

        orgHandler.updateBasic(org, request.getName()
                , request.getLeader(), userId);

        orgRepository.update(org);

        return buildOrgDto(org);
    }

    //取消组织
    @Transactional
    public Long cancelOrg(Long id, Long tenant, Long userId) {
        Org org = orgRepository.findById(tenant, id)
                .orElseThrow(() -> {
                    throw new BusinessException("要取消的组织(id ="
                                + id + "  )不存在！");
                });

        orgHandler.cancel(org, userId);
        orgRepository.update(org);

        return org.getId();
    }

    private static OrgDto buildOrgDto(Org org) {
        // 将领域对象转换成DTO
    }

}

```

我们写了一个 OrgHandler 来协助完成功能。和 Validator 一样，这也是一个领域服务，但命名为 OrgDomainService 显得有点繁琐，我们这里按 XxxHandler 命名，也就是 Xxx 处理器。

OrgHandler 的代码是这样的。

```java

package domain.orgmng.org;
// imports...

@Component
public class OrgHandler {
    private final CommonValidator commonValidator;
    private final OrgNameValidator nameValidator;
    private final OrgLeaderValidator leaderValidator;
    private CancelOrgValidator cancelValidator;

    public OrgHandler(CommonValidator commonValidator
            , OrgNameValidator nameValidator
            , OrgLeaderValidator leaderValidator
            , CancelOrgValidator cancelValidator) {
        // 为依赖注入赋值...
    }

    public void updateBasic(Org org, String newName
                , Long newLeader, Long userId) {
        updateName(org, newName);
        updateLeader(org, newLeader);
        updateAuditInfo(org, userId);
    }

    public void cancel(Org org, Long userId) {
        cancelValidator.cancelledOrgShouldNotHasEmp(org.getTenant()
                        , org.getId());
        cancelValidator.OnlyEffectiveOrgCanBeCancelled(org);
        org.setStatus(OrgStatus.CANCELLED);
        updateAuditInfo(org, userId);
    }

    private void updateLeader(Org org, Long newLeader) {
        if (newLeader != null && !newLeader.equals(org.getLeader())) {
            leaderValidator.leaderShouldBeEffective(org.getTenant()
                                , newLeader);
            org.setLeader(newLeader);
        }
    }

    private void updateName(Org org, String newName) {
        if (newName != null && !newName.equals(org.getName())) {
            nameValidator.orgNameShouldNotEmpty(newName);
            nameValidator.nameShouldNotDuplicatedInSameSuperior(
                  org.getTenant(), org.getSuperior(), newName);
            org.setName(newName);
        }
    }

    private void updateAuditInfo(Org org, Long userId) {
        // 设置最后修改人和时间
    }
}
```

与 OrgBuilder 类似，这个类中的方法基本上也是先校验，再赋值，只不过这里是更改而不是新建。同时 OrgHandler 没有可变属性，因此可以直接注入到应用服务。
## 提高程序的封装性
### 提高应用 API 的封装性


所谓“封装”，指的是将一个模块的实现细节尽量隐藏在内部，只向外界暴露最小的可访问接口，也叫信息隐藏原则，或最小接口原则，目的是减小模块间的耦合，提高程序的可维护性。这里说的模块是广义的，一个函数、一个类、一个包乃至整个应用系统，都可以看作模块，而我们之前领域建模中说的模块模式是狭义的，专门指领域模型里的领域对象所组成的包。

先看一下系统对外提供的 API。我们的系统采用 RestfulAPI，以 JSON 作为参数格式，JSON 的结构则是与 OrgDto 一致的。目前的 DTO 中有领域对象中所有 11 个属性，因此，不论增加还是修改，入口参数的 JSON 格式都是下面这样。

```json
{
  "createdAt": "2022-10-05T06:49:39.659Z",
  "createdBy": 0,
  "id": 0,
  "lastUpdatedAt": "2022-10-05T06:49:39.659Z",
  "lastUpdatedBy": 0,
  "leader": 0,
  "name": "string",
  "orgType": "string",
  "status": "string",
  "superior": 0,
  "tenant": 0
}
```
我们已经知道，添加和修改操作，实际上都只是用了这些属性的一个子集。现在这样简单粗暴地把所有属性都暴露给客户端，不仅违反了最小接口原则，也容易在理解上发生混淆。


为了解决这个问题，我们要为每个功能编写单独的参数 DTO，只包含必要的参数。DTO 本身的修改比较简单，你可以自己试一下。这里我们重点来看一下修改后应用层的包结构。

<img src="https://wkq-img.oss-cn-chengdu.aliyuncs.com/20241203003214.png"/>

CreateOrgRequest 和 UpdateOrgBasicRequest 分别是添加和修改组织的参数 DTO。原来的 OrgDto 改名为 OrgResponse，作为两个功能共同的返回参数类型。另外，这里又出现了一个包结构打横分还是打竖分的问题。

一些伙伴更喜欢打横分，也就是创建一个 dto 包，把 EmpService 和 OrgService 的 DTO 都放进去。而我们这里采用的是打竖分，也就是把 service 和这个 service 用到的 DTO 放在同一个包内，提高了模块的内聚性。

修改之后，添加组织的 JSON 参数格式成了下面这样。

```json
{
  "leader": 0,
  "name": "string",
  "orgType": "string",
  "superior": 0,
  "tenant": 0
}
```

这就简洁多了，关键是缩小了 API，系统的封装性提高了。修改组织功能的 JSON 的优化也是类似的。

### 提高领域对象的封装性

改进了系统 API 的封装之后，我们再来考虑领域对象层面。

现在的 Org 对象只是纯粹的 DTO，所有属性都通过 getter 和 setter 暴露在外，没有任何封装性可言。要提高封装性，可以从两个角度考虑。第一是限制 getter 和 setter 的数量；第二是用表示业务含义的接口代替简单的 setter 和 getter。

由于 getter 只是用来查询，不会破坏数据，而不恰当的 setter 则可能破坏数据，导致程序出错，所以相对而言，限制 setter 比限制 getter 更重要一些。为了限制 setter，我们再列一个表，研究一下在创建了组织以后，哪些属性是可以修改的。

<img src="https://wkq-img.oss-cn-chengdu.aliyuncs.com/20241203003513.png"/>

我们可以只为那些可以修改的属性保留 setter，其他的只有 getter，成为只读属性。再为 Org 类增加一个包含只读属性的构造器，以便创建对象。

```java title='修改后的 Org 类'
public class Org {
    private Long id;
    private Long tenantId;
    private Long superiorId;
    private String orgTypeCoDe;
    private Long leaderId;
    private String name;
    private OrgStatus status;
    private LocalDateTime createdAt;
    private Long createdBy;
    private LocalDateTime lastUpdatedAt;
    private Long lastUpdatedBy;

    public Org(Long tenantId, String orgTypeCoDe
          , LocalDateTime createdAt, Long createdBy) {
        // 为属性赋值 ...
    }

    // 所有属性的 getter ...

    // 保留了 superiorId, leaderId, name,
    // lastUpdateAt, lastUPdateBy 和 status 的 setter ...

}
```
其中 OrgId 比较特殊，大部分情况下是只读的，但当 Repository 将新建的 Org 保存到数据库时，由于 id 是数据库自动生成的，需要回填 id 值。这可以通过反射等技巧来绕过去。事实上，很多数据库访问框架就是利用反射，直接为私有属性赋值，以便在不破坏封装的前提下，从数据库中取出对象。

<RedSpan>通过减少领域对象的 Setter，我们进一步提高了程序的封装性</RedSpan>。

### 通过“表意接口”提高封装性


做了这些改进以后，我们再看看在 OrgHandler 中完成“撤销组织”功能的代码。

```java
package domain.orgmng.org;
// imports ...

@Component
public class OrgHandler {
    //...

    public void cancel(Org org, Long userId) {
        cancelValidator.OrgToBeCancelledShouldNotHasEmp(
                                    org.getTenantId(), org.getId());
        cancelValidator.OrgToBeCancelledShouldBeEffective(org);
        org.setStatus(OrgStatus.CANCELLED); // 直接为 Status 赋值
        updateAuditInfo(org, userId);
    }

    // ...
}
```

其中，org.setStatus(OrgStatus.CANCELLED) 直接将组织的状态设置成了“已撤销”。从面向对象的角度来看，更好的做法是 Org 类提供一个 cancel() 方法，像下面这样：

```java
package domain.orgmng.org;
// imports ...

public class Org {
    //...

    //Org 自己管理自己的状态
    public void cancel() {
        this.status = OrgStatus.CANCELLED;
    }

    //...
}
```

这样，Org 类就可以自己管理自己的状态，OrgHandler 就不必了解 Org 内部状态的转换细节，只需告诉 Org 需要撤销就可以了，像下面这样。


```java
package domain.orgmng.org;
// imports ...

@Component
public class OrgHandler {
    //...

    public void cancel(Org org, Long userId) {
        cancelValidator.OrgToBeCancelledShouldNotHasEmp(
                        org.getTenant(), org.getId());
        cancelValidator.OrgToBeCancelledShouldBeEffective(org);
        org.cancel();   // 只需告诉 Org 要进行撤销，但不必了解 Org 内部细节
        updateAuditInfo(org, userId);
    }

    // ...
}
```


类似的，我们看一下“只有有效的组织才能被撤销”这条规则的实现代码。

```java
package domain.orgmng.org.validator;
// imports...

@Component
public class CancelOrgValidator {
    //...

    // 只有有效的组织才能被撤销
    public void OnlyEffectiveOrgCanBeCancelled(Org org) {
        //直接访问了状态属性
        if (!org.getStatus().equals(OrgStatus.EFFECTIVE)) {
            throw new BusinessException("该组织不是有效状态，不能撤销！");
        }
    }

    //...
}
```

我们看到 CancelOrgValidator 直接访问了 Org 的状态，判断是否为有效组织。这实际上又是重构中的一种坏味道，叫做特性依恋（Feature Envy）。Status 这个特性是属于 Org 类的，而 CancelOrgValidator 要通过访问这个特性来实现自己的逻辑，所以 CancelOrgValidator “依恋”了 Org 的特性。这是对象封装性被破坏的征兆。

解决方法是将这段判断逻辑移动到 Org 类内部，重构后的 Org 类是这样的。

```java
package chapter12.unjuanable.domain.orgmng.org;
// imports ...

public class Org {
    //...

    public boolean isEffective() {
        return status.equals(OrgStatus.EFFECTIVE);
    }

    //...
}
```
这样，CancelOrgValidator 就可以不依赖 Org 的内部状态了。
```java
package chapter12.unjuanable.domain.orgmng.org.validator;
// imports...

@Component
public class CancelOrgValidator {
    //...

    // 只有有效的组织才能被撤销
    public void OnlyEffectiveOrgCanBeCancelled(Org org) {
        //不再依赖 Org 的内部状态
        if (!org.isEffective()) {
            throw new BusinessException("该组织不是有效状态，不能撤销！");
        }
    }

    //...
}
```

Org.cancel() 和 Org.isEffective() 既提高了对象的封装性，也表达了这个功能的业务含义，因此也是 <RedSpan>表意接口模式</RedSpan>的一种应用。

上一节的表意接口体现在“领域服务”，这一节体现在“领域对象”。对“表意接口”的运用，往往可以避免“特性依恋”的坏味道，反之，发现和消除“特性依恋”，也会重构出“表意接口”。

## 用继承消除重复

下面再来看看 Org 类还有什么可以优化的地方。

我们发现，其实每个实体都包含 4 个审计字段 createdAt、createdBy、lastUpdatedAt 和 lastUpdatedBy。那么我们可以考虑抽出一个包含这四个属性的父类，减少编写各个类的重复工作。抽出的父类是下面的样子。


```java
package common.framework.domain;

import java.time.LocalDateTime;

public abstract class AuditableEntity {
    protected LocalDateTime createdAt;
    protected Long createdBy;
    protected LocalDateTime lastUpdatedAt;
    protected Long lastUpdatedBy;

    public AuditableEntity(LocalDateTime createdAt, Long createdBy) {
        this.createdAt = createdAt;
        this.createdBy = createdBy;
    }

    // lastUPdatedAt 和 lastUpdatedBy 的 setter 以及所有属性的 getter ...

}
```

这个类放在 common.framework 包里面，因为这种基类属于框架层面。


在面向对象设计中一个常见的陷阱就是滥用继承。要防止这一倾向，要记住一个原则，<RedSpan>要仅仅为了复用而使用继承</RedSpan>。你还要问自己一个问题，父类和子类的关系，在语义上，<RedSpan>是否有分类关系，或者概念的普遍和特殊关系</RedSpan>。只有符合这种关系的，才能采用继承，否则应该用“组合”来实现复用。

在我们的例子中，我们发现审计字段可以复用，只是可能可以采用继承的一个征兆。然后我们再从语义上进行分析。可以说，每个实体都是可审计的，或者说，每类实体都是一类特殊的可审计实体。<RedSpan>我们发现了这种普遍和特殊的分类关系，所以在这里运用继承是合理的</RedSpan>。



## 编程风格回顾


具体的编程风格是多种多样的，每种都各有利弊。因此，不可能也没有必要强求一致。关键是理解背后的原理，作出取舍，然后在自己的开发团队中形成比较统一的风格。

### 领域对象不访问数据库
我们在领域对象中既不会显式，也不会隐式访问数据库，目的是使领域对象和数据库解耦，便于维护和测试。

如果使用 JPA 并结合延迟加载，领域对象就会隐式地访问数据库；如果在领域对象的方法中写 SQL，则会显式地访问数据库。领域对象访问数据库，才能实现更加纯粹的面向对象风格，代价是一般需要对 ORM 框架有深入的理解，或者增加了领域对象和数据库访问机制的耦合，还可能无意间导致性能等问题。你可以根据这两者的利弊进行取舍。

### 领域服务只能读数据库
在实现一些业务规则时，需要访问数据库中的数据，因此领域服务需要读数据库。而写库的功能通常可以由应用服务来做，从而减轻领域层的负担。有些伙伴喜欢把写功能也放在领域服务，从而使应用服务非常薄，这样也可以。

### 应用服务可以读写数据库。

纯粹的（通过调用仓库）读写数据库并不包含领域知识，因此放在应用服务似乎更合适一些。比如更新一个对象前，要把这个对象先从数据库中读出来；创建或修改对象后，要存回数据库，这些本身都没有领域逻辑。

### 用 ID 表示对象之间的关联。

如果按偏面向对象的风格，对象之间的关联应该用对象导航的方式来实现。比如说，Org 对象的 leader 属性的类型，应该是 Emp（员工），这样就可以在内存中直接从组织对象导航到充当负责人的员工对象了。不过我们的程序中，leader 属性仅保存了员工的 ID，而不是员工对象。这是考虑到在企业应用中节省带宽和内存。关于对象导航风格，我们会在下个迭代中继续探讨。

### 领域对象有自己的领域服务。

如果是偏面向对象风格，很多领域逻辑可以在领域对象内部完成，因此不一定需要领域服务。但对于偏过程式的风格，由于领域对象不能访问数据库，很多领域逻辑就要外化到领域服务中，因此多数领域对象都会有相应的领域服务。

### 在以上前提下利用封装和继承。

即使是我们这种偏过程式的风格，如果善于利用封装、合理利用继承，也能有效提高程序质量。一个技巧是识别特性依恋的坏味道，并重构到表意接口。

## 总结

这节我们主要是利用封装和继承，进一步提高了代码质量。我们可以通过两个层面来提高封装性。

第一个层面是 API 的封装。添加和修改组织两种 API 的参数是不同的，我们只对外暴露出每个 API 必须的参数，从而缩小了接口，提高了封装性。

第二个层面是领域对象的封装。我们分析了哪些属性是不需要修改的，把这些属性变成只读的，从而缩小了对象的接口。另一方面，我们用表意接口封装了状态属性，使外界不需要直接关心状态转换的细节，同时消除了特性依恋的坏味道。

另外，我们还可以利用继承减少代码的重复。在我们的例子里，是通过抽象出 AuditableEntity 作为所有实体的父类来实现的。

第一个迭代就结束了。通过实现组织的创建和修改功能，我已经把这个迭代需要掌握的知识点都介绍清楚了（为了让你关注重点，我没有把所有的代码实现都写进课程里）。你可以尝试运用这些知识，自己实现其他的功能。学完这个迭代，应该可以利用 DDD 开发一些不太复杂的需求了，你不妨在自己的项目中试一试。

:::tip
依赖数据库的领域逻辑放到领域服务中，否则放到领域对象中。
:::

:::tip
对于领域对象，可以利用 Java 的包级私有权限，进一步增强封装。
```java
// 在 com.example.domain 包中
package com.example.domain;

public class User {
    // 包级私有字段
    String name;
    int age;

    // 包级私有构造函数
    User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 包级私有方法
    void printUserInfo() {
        System.out.println("Name: " + name + ", Age: " + age);
    }
}

// 在同一包中
package com.example.domain;

public class UserService {
    public void createUser(String name, int age) {
        User user = new User(name, age); // 可以访问包级私有的构造函数
        user.printUserInfo(); // 可以调用包级私有的方法
    }
}
```
:::